﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace SharpNoisePCL.Noise
{

    public class WorleyNoise : INoiseGenerator
    {
        private class Point
        {
            public float x, y, z;
        }

        private float[] DistanceArray = new float[3];

        public int DistanceFunction = 0;
        public int CombineDistanceFunction = 0;
        public int Seed = 3221;

        uint OFFSET_BASIS = 2166136261;
        uint FNV_PRIME = 16777619;

        float CombinerFunc1(float[] array)
        {
            return array[0];
        }

        float CombinerFunc2(float[] array)
        {
            return array[1] - array[0];
        }

        float CombinerFunc3(float[] array)
        {
            return array[2] - array[0];
        }

        float EuclidianDistanceFunc(Point p1, Point p2)
        {
            return (p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y)
                + (p1.z - p2.z) * (p1.z - p2.z);
        }

        float ManhattanDistanceFunc(Point p1, Point p2)
        {
            return Math.Abs(p1.x - p2.x) + Math.Abs(p1.y - p2.y) + Math.Abs(p1.z - p2.z);
        }

        float ChebyshevDistanceFunc(Point p1, Point p2)
        {
            Point diff = new Point() { x = p1.x - p2.x, y = p1.y - p2.y, z = p1.z - p2.z };
            return Math.Max(Math.Max(Math.Abs(diff.x), Math.Abs(diff.y)), Math.Abs(diff.z));
        }

        long probLookup(long value)
        {
            value = value & 0xffffffff;
            if (value < 393325350) return 1 & 0xffffffff;
            if (value < 1022645910) return 2 & 0xffffffff;
            if (value < 1861739990) return 3 & 0xffffffff;
            if (value < 2700834071) return 4 & 0xffffffff;
            if (value < 3372109335) return 5 & 0xffffffff;
            if (value < 3819626178) return 6 & 0xffffffff;
            if (value < 4075350088) return 7 & 0xffffffff;
            if (value < 4203212043) return 8 & 0xffffffff;
            return 9 & 0xffffffff;
        }

        void insert(float[] arr, float value)
        {
            float temp;
            for (var i = arr.Length - 1; i >= 0; i--)
            {
                if (value > arr[i]) break;
                temp = arr[i];
                arr[i] = value;
                if (i + 1 < arr.Length) arr[i + 1] = temp;
            }
        }

        long lcgRandom(long lastValue)
        {
            return (((1103515245 & 0xffffffff) * lastValue + (12345 & 0xffffffff)) % 0x100000000) & 0xffffffff;
        }


        long hash(long i, long j, long k)
        {
            return ((((((OFFSET_BASIS ^ (i & 0xffffffff)) * FNV_PRIME) ^ (j & 0xffffffff)) * FNV_PRIME)
                ^ (k & 0xffffffff)) * FNV_PRIME) & 0xffffffff;
        }

        float noise(Point input)
        {
            var value = 0;

            long lastRandom;
            long numberFeaturePoints;
            Point randomDiff = new Point() { x = 0, y = 0, z = 0 };
            Point featurePoint = new Point() { x = 0, y = 0, z = 0 };

            int cubeX, cubeY, cubeZ;

            for (var i = 0; i < DistanceArray.Length; i++)
            {
                this.DistanceArray[i] = 6666;
            }

            var evalCubeX = (int)(Math.Floor(input.x));
            var evalCubeY = (int)(Math.Floor(input.y));
            var evalCubeZ = (int)(Math.Floor(input.z));

            for (var i = -1; i < 2; ++i)
                for (var j = -1; j < 2; ++j)
                    for (var k = -1; k < 2; ++k)
                    {
                        cubeX = evalCubeX + i;
                        cubeY = evalCubeY + j;
                        cubeZ = evalCubeZ + k;

                        //2. Generate a reproducible random number generator for the cube
                        lastRandom = lcgRandom(hash((cubeX + this.Seed) & 0xffffffff, (cubeY) & 0xffffffff, (cubeZ) & 0xffffffff));
                        //3. Determine how many feature points are in the cube
                        numberFeaturePoints = probLookup(lastRandom);
                        //4. Randomly place the feature points in the cube
                        for (var l = 0; l < numberFeaturePoints; ++l)
                        {
                            lastRandom = lcgRandom(lastRandom);
                            randomDiff.x = lastRandom / 0x100000000;

                            lastRandom = lcgRandom(lastRandom);
                            randomDiff.y = lastRandom / 0x100000000;

                            lastRandom = this.lcgRandom(lastRandom);
                            randomDiff.y = lastRandom / 0x100000000;

                            featurePoint
                                = new Point { x = randomDiff.x + cubeX, y = randomDiff.y + cubeY, z = randomDiff.z + cubeZ };

                            //5. Find the feature point closest to the evaluation point. 
                            //This is done by inserting the distances to the feature points into a sorted list
                            float v = EuclidianDistanceFunc(input, featurePoint);
                            this.insert(DistanceArray, v);
                        }
                        //6. Check the neighboring cubes to ensure their are no closer evaluation points.
                        // This is done by repeating steps 1 through 5 above for each neighboring cube
                    }

            var color = CombinerFunc1(DistanceArray);
            if (color < 0) color = 0;
            if (color > 1) color = 1;

            return color;
        }

        public float Get1D(float x)
        {
            return noise(new Point() { x = x, y = 0, z = 0 });
        }

        public float Get2D(float x, float y)
        {
            return noise(new Point() { x = x, y = y, z = 0 });
        }

        public float Get3D(float x, float y, float z)
        {
            return noise(new Point() { x = x, y = y, z = z });
        }

        public float Get4D(float x, float y, float z, float t)
        {
            throw new NotImplementedException();
        }
    }
}
